Skip to content

Rules of Hooks

So, we've talked about how hooks are special functions that allow us to “hook” into React internals. useState allows us to hook into a component instance's state, for example, while useId allows us to create and store a unique identifier on the component instance.

What happens if we try to call these functions outside of a React context?

Well, let's try it:

Code Playground

import React from 'react';

try {
React.useId();
} catch (err) {
// Swallowing an error that occurs because
// of the warning shown in the console.
}
console

Lint Warning

  • React Hook "React.useId" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.

    Rule: react-hooks/rules-of-hooks

    Location: Line 4, Column 3

  1. Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

In addition to the lint error, we're given a console message that looks like this:

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app

It shows that something's gone horribly wrong, and provides 3 possibilities. Errors 1 and 3 refer to rare edge-case concerns, but that second bullet point is interesting. What are the “Rules of Hooks”?

First, let's understand that hooks are plain old JavaScript functions. They aren't quite as magical as they might seem.

But, when we call these functions, they "hook into" React internals. And we can catch React off-guard. It expects these hook functions to be used in very specific ways, and if we violate those expectations, bad things happen.

There are two “Rules of Hooks” that we should learn, in order to make sure we're always using hooks as React expects.

  1. Hooks have to be called within the scope of a React application. We can't call them outside of our React components.
  2. We have to call our hooks at the top level of the component.

That second rule is the one that trips most people up. Let's talk about it.

Video Summary

Suppose we have the following code:

function TextInput({ id, label, type }) {
let appliedId = id;
if (typeof appliedId === 'undefined') {
appliedId = React.useId();
}
return (
<div className="text-input">
<label htmlFor={appliedId}>
{label}
</label>
<input
id={appliedId}
type={type}
/>
</div>
);
}

The TextInput component takes an optional id prop. If it's not provided, we'll generate one automatically.

This seems like the most reasonable thing in the world, but there's a big problem here: we're violating one of the rules of hooks.

The rule states that we're not allowed to use the hook conditionally. We're never supposed to put a hook inside an if condition, or a switch statement, or a for loop, or even inside a callback.

This seems very arbitrary, doesn't it?

Explaining why this rule exists requires diving deep into a rabbit hole, and I don't want to go that deep just yet, but let's quickly discuss the general idea.

Let's say we're building a component with 2 pieces of state:

function TextInput({ id, label, type }) {
const [x, setX] = React.useState();
const [y, setY] = React.useState();
}

We call useState() twice, since we have 2 different state variables.

Here's the question: how does React know which piece of state we're requesting?

Notice that we aren't giving React any sort of unique ID for each piece of state. We're not doing this:

const [x, setX] = React.useState({ id: 'x' });
const [y, setY] = React.useState({ id: 'y' });

React uses the order of the function calls to figure out which state to provide to each hook.

If we render a component 100 times, it should call the exact same hooks in the exact same order. When we render a hook conditionally, we make it possible for the hooks to change from one render to another!

This leads to very confusing bugs, where the y variable could be assigned to the x state.

This rule applies to all hooks, not just useState.

So how do we solve our original problem? Here's the solution:

function TextInput({ id, label, type }) {
let generatedId = React.useId();
let appliedId = id || generatedId;
return (
<div className="text-input">
<label htmlFor={appliedId}>
{label}
</label>
<input
id={appliedId}
type={type}
/>
</div>
);
}

In this revised version, we're always calling React.useId(), no matter whether they've supplied an ID or not.

It might seem wasteful to be generating an ID even if we don't need it, but fortunately it's a very quick process. It has zero noticeable impact on performance.

Here's the sandbox from the video:

Code Playground

import React from 'react';

function TextInput({ id, label, type }) {
// Here's the original code, violating the rule:
//
// let appliedId = id;
// if (typeof appliedId === 'undefined') {
// appliedId = React.useId();
// }
//
// ...and here's the fixed code:
const generatedId = React.useId();
const appliedId = id || generatedId;

return (
<div className="text-input">
<label htmlFor={appliedId}>
{label}
</label>
<input
id={appliedId}
type={type}
/>
</div>
);
}

export default TextInput;